package network;

import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;

import javax.swing.Timer;

import model.Acknowledgement;
import model.Email;
import model.EmailImp;
import model.MeetingInfo;
import model.MeetingMail;

import requestManager.EmailApplicationRequestManager;
import resources.ResourceProvider;
import textUtil.Compare;

/**
 * Network Server client
 */
public class NetworkServerClientRequestHandler 
{
	public static final boolean isMainServer = true;
    private volatile boolean isServerOn = true;
	private Map<String,String> ID_IP_Table;
	private boolean isServer;
    private Map<Object, List<String>> failedList; 
    private Timer retryTimer;
	private List<Observer> clients;
	private List<Observer> servers;
	private EmailApplicationRequestManager emailAppController;
	private Map<Integer,List<Map<Integer,String>>> meetingUpdateChanges = new LinkedHashMap<Integer,List<Map<Integer,String>>>();

	/**
	 * Constructor
	 */
	public NetworkServerClientRequestHandler()
	{
	    this.clients = new LinkedList<Observer>();
	    this.servers = new LinkedList<Observer>();
	    
	    this.ID_IP_Table = new LinkedHashMap<String,String>();
	    
	    this.failedList = Collections.synchronizedMap(new LinkedHashMap<Object, List<String>>());
	    
	    
	    if(isMainServer)
	    {
	        this.servers.add(new ServerMessageReceivedCallback());
	        this.runMainServer();
	        retryTimer = new Timer(1000, new ActionListener()
	          {
	              @Override
	              public void actionPerformed(ActionEvent e)
	              {
	                  if(!NetworkServerClientRequestHandler.this.failedList.isEmpty())
	                  {
	                      for(Object obj : NetworkServerClientRequestHandler.this.failedList.keySet())
	                      {
	                          if(obj instanceof EmailImp)
	                          {
	                              final EmailImp email = (EmailImp) obj;
	                              final List<String> recepients = Collections.synchronizedList(NetworkServerClientRequestHandler.this.failedList.get(email));
	                              
	                              for(String receiver : recepients)
	                              {  
	                                  String recR = receiver;
	                                  
	                                  boolean isSent = NetworkServerClientRequestHandler.this.sendDataToClient(recR, email, ResourceProvider.CLIENT_PORT);
	                                  if(isSent)
	                                  {
	                                      recepients.remove(recR);
	                                  }
	                              }
	                              
	                              if(recepients.isEmpty())
	                              {
	                                  NetworkServerClientRequestHandler.this.failedList.remove(email);
	                              }
	                          }
	                          else if(obj instanceof Acknowledgement)
	                          {
	                              Acknowledgement ack = (Acknowledgement) obj;
	                              List<String> recepient = NetworkServerClientRequestHandler.this.failedList.get(ack);
	                              NetworkServerClientRequestHandler.this.sendDataToClient(recepient.get(0), obj, ResourceProvider.CLIENT_PORT);
	                          }
	                      }
	                  }
	              }
	          });
	          retryTimer.start();
	    }   
	}
	
	/**
	 * add observer
	 * @param observer
	 */
	public void addObserver(Observer observer)
	{
	    clients.add(observer);
	}
	
	/**
	 * check if server is ON
	 * @return returns either true or false
	 */
	public boolean isServerOn()
	{
		return this.isServerOn;
	}
	
	/**
	 * set server ON flag - true/false
	 * @param state
	 */
	public void setServerOn(boolean state)
	{
		this.isServerOn = state;
	}
	
	
	/**
     * run server
     */
    public void runMainServer()
    {
        Thread serverThread = new Thread()
        {
            public void run() 
            {
                try
                {
                    ServerSocket serv = new ServerSocket(ResourceProvider.MAIN_SERVER_PORT);
                    Socket serverSocket;
                    ObjectInputStream inputStreamFromClient;
                    Object objectReceived;
                    while(isMainServer)
                    {
                        serverSocket = serv.accept();
                        inputStreamFromClient = new ObjectInputStream(serverSocket.getInputStream());
                        objectReceived = inputStreamFromClient.readObject();

                        for(Observer observer : servers)
                        {
                            observer.update(null, objectReceived);
                        }
                    }    
                }
                catch(ClassNotFoundException cnfe)
                {
                    cnfe.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                catch(IOException e)
                {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
            }
        };
        
        serverThread.start();  
    }
	
	/**
	 * run server
	 */
	public void runServer()
	{
	    Thread serverThread = new Thread()
	    {
	        public void run() 
	        {
	            try
	            {
	                ServerSocket serv = new ServerSocket(9110);
	                Socket serverSocket;
	                ObjectInputStream inputStreamFromClient;
	                Object objectReceived;
	                while(isServerOn)
	                {
	                    serverSocket = serv.accept();
	                    inputStreamFromClient = new ObjectInputStream(serverSocket.getInputStream());
	                    objectReceived = inputStreamFromClient.readObject();

	                    for(Observer observer : clients)
	                    {
	                        observer.update(null, objectReceived);
	                    }
	                }    
	            }
	            catch(ClassNotFoundException cnfe)
	            {
	                cnfe.printStackTrace();
	                Thread.currentThread().interrupt();
	            }
	            catch(IOException e)
	            {
	                e.printStackTrace();
	                Thread.currentThread().interrupt();
	            }
	        }
	    };
	    
	    serverThread.start();  
	}
	
	/**
	 * send data to the client
	 * @param userID user id
	 * @param objectToTransmit object data to transmit
	 * @param port the port number
	 * @return return true if successful, otherwise false
	 */
	public boolean sendDataToClient(String userID, Object objectToTransmit, int port)
	{ 
        	    boolean isClientAvailable = NetworkServerClientRequestHandler.this.isClientAvailable(userID);
                if(isClientAvailable)
                {
                    
                    String toSendToIP = ID_IP_Table.get(userID);
                    Socket clientSocket = null;
             	    try
                     {
                         InetAddress addr =InetAddress.getByName(toSendToIP);
             	         clientSocket = new Socket(addr, port);
                         ObjectOutputStream outputStreamToServer = new ObjectOutputStream(clientSocket.getOutputStream());
                         outputStreamToServer.writeObject(objectToTransmit);
                         outputStreamToServer.flush();
                         return true;
                     } 
             	    catch (IOException e) 
                     {
             	        // failed to send
             	    	NetworkServerClientRequestHandler.this.failedToSendToClient(userID,objectToTransmit);
                        return false;
                     }
             	    finally
             	    {
             	    	if(clientSocket != null)
             	    	{
             	    		try 
             	    		{
								clientSocket.close();
							} 
             	    		catch (IOException e) 
							{
								e.printStackTrace();
							}
             	    	}
             	    }
             	    
                }
                else
                {
                    // client not available
         	    	NetworkServerClientRequestHandler.this.failedToSendToClient(userID,objectToTransmit);
                    return false;
                }
	}
	
	/**
	 * method perform necessary tasks while failed to send the email/acknowledgment to the client
	 * @param userID
	 * @param objectToTransmit
	 */
	private void failedToSendToClient(String userID, Object objectToTransmit) 
	{
		if(objectToTransmit instanceof EmailImp)
        {
			if(failedList.containsKey(objectToTransmit))
	        {
	            	EmailImp email = (EmailImp) objectToTransmit;
	            	List<String> list = failedList.get(email);
	            	if(!list.contains(userID))
                    {
	                   list.add(userID);
                    }
	        }
	        else
	        {
	        		EmailImp email = (EmailImp) objectToTransmit;
	            	List<String> list = new LinkedList<String>();
	                list.add(userID);
		            failedList.put(email, list);	  
	        }
        }
		else if(objectToTransmit instanceof Acknowledgement)
		{
			if(!failedList.containsKey(objectToTransmit))
	        {
				List<String> list = new LinkedList<String>();
	            list.add(userID);
	        	failedList.put(objectToTransmit,list);
	        }		
		}		
	}

	/**
	 * check if client is available
	 * @param userID user id
	 * @return true if client is available
	 */
	public boolean isClientAvailable(String userID)
	{
        try
        {
            String toSendToIP = ID_IP_Table.get(userID);
            InetAddress addR =InetAddress.getByName(toSendToIP);            
            if (addR.isReachable(ResourceProvider.NETWORK_WAITING_PERIOD)==true)  
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        catch (UnknownHostException e)
        {
            return false;
        }
        catch (IOException e)
        {
            return false;
        }
	}
	
	/**
	 * get IP address map
	 * @return the map of the user id IP address table
	 */
	public Map<String,String> getIdIpTable()
    {
         return this.ID_IP_Table;
    }
	
	/**
	 * is server enabled
	 * @return return true if enabled, otherwise false
	 */
	public boolean isServer()
	{
	    return this.isServer;
	}
    
	/**
	 * update IP map
	 * @param entry the entry  to update
	 */
    public void updateWith(IpTableEntry entry)
    {
        this.ID_IP_Table.put(entry.userID, entry.ipAddres);
    }
    
    /**
     * get current system IP
     * @return return the IP address
     */
    public static String ip()
    { 
        String toReturn = "";
        Enumeration<NetworkInterface> interfaces = null;
        try {
            interfaces = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e1) {
            
            e1.printStackTrace();
        }
        while (interfaces.hasMoreElements())
        {
            NetworkInterface current = interfaces.nextElement();
    
            try 
            {
                if (!current.isUp() || current.isLoopback() || current.isVirtual()) continue;
            } catch (SocketException e) {
                e.printStackTrace();
            }
            
            Pattern pattern = Pattern.compile("^(\\d{1,3}\\.){3}\\d{1,3}$");
            Enumeration<InetAddress> addresses = current.getInetAddresses();
            
            while (addresses.hasMoreElements())
            {
                InetAddress current_addr = addresses.nextElement();
                if (current_addr.isLoopbackAddress()) continue;
                Matcher matcher = pattern.matcher(current_addr.getHostAddress());
                
                if(matcher.find())
                    toReturn= current_addr.getHostAddress();
            }
        }
        return toReturn;
    }

    /**
     * send email through server
     * @param email
     */
    public void sendEmailThroughServer(EmailImp email)
    {
        this.sendDataToClient(ResourceProvider.SERVER, email,ResourceProvider.MAIN_SERVER_PORT);
    }

    public void broadCastEmailToClient(final EmailImp email)
    {
        Timer timer = new Timer(0, new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                    List<String> recepients = email.getReceiver();
                    
                    for(String reciever : recepients)
                    {  
                        NetworkServerClientRequestHandler.this.sendDataToClient(reciever,email, ResourceProvider.CLIENT_PORT);
                    }
            }
        });
        timer.setRepeats(false);
        timer.start();           
    }
    
    /**
     * Callback Class for Server to receive email/acknowledgement
     *
     */
    public class ServerMessageReceivedCallback implements Observer
    {
                
        /**
         * Constructor
         */
        public ServerMessageReceivedCallback()
        {
        }
       
        /*
         * (non-Javadoc)
         * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
         */
        public void update(Observable o, Object receivedObject)
        {
                if(receivedObject instanceof EmailImp)
                {
                    final EmailImp email = (EmailImp) receivedObject;
                    
                    Thread thread = new Thread()
                    {
                        @Override
                        public void run()
                        {
                            Acknowledgement ack = new Acknowledgement(Acknowledgement.ACK_EMAIL_SENT_SUCCESSFULLY,email.getPersistencePath());
                            NetworkServerClientRequestHandler.this.sendDataToClient(email.getSender() ,ack, ResourceProvider.CLIENT_PORT);
                            
                            NetworkServerClientRequestHandler.this.broadCastEmailToClient(email);
                        }
                    };
                    thread.start();
                    
                    Thread thread1 = new Thread()
                    {
                        @Override
                        public void run()
                        {
                            if(email instanceof MeetingMail)
                            {
                                MeetingMail meetingMail = (MeetingMail) email;
                                String meetingID = meetingMail.getMeetingId();
                                String meetingPath = ResourceProvider.getRootFolderPath(ResourceProvider.SERVER_MEETING_FOLDER, NetworkServerClientRequestHandler.this.emailAppController.getUser());
                                String meetingDataPath = meetingPath + ResourceProvider.getFilePathSeperator() + meetingID + ".json";
                                //check if meetingID is already have meeting info
                                if(!new File(meetingDataPath).exists())
                                {
                                    //creating for the first time
                                    MeetingInfo info = new MeetingInfo(meetingID);
                                    info.setOrigianlPlace(meetingMail.getMeetingPlace());
                                    info.setOriginalMessage(meetingMail.getMessage());
                                    info.setOriginalStartTime(meetingMail.getStartTime());
                                    info.setOriginalEndiTime(meetingMail.getEndTime());
                                    info.setOriginalSubject(meetingMail.getSubject());
                                    info.setReceivers(meetingMail.getReceiver());
                                    info.setSender(meetingMail.getSender());
                                    NetworkServerClientRequestHandler.this.emailAppController.requestPersistMeetingInfo(info, meetingPath);
                                } 
                            }
                        }
                    };
                    thread1.start(); 
                }
                else if(receivedObject instanceof Acknowledgement)
                {
                    final Acknowledgement ack = (Acknowledgement) receivedObject;
                    
                    if(ack.action == Acknowledgement.ACK_MEETING_UPDATED_By_USER)
                    {
                        Thread thread = new Thread()
                        {
                          @Override
                         public void run()
                         {
                            Map<Integer,String> changes = (Map<Integer, String>) ack.receivedObj;
                            NetworkServerClientRequestHandler.this.updateMeetingInformation(changes);
                         }  
                        };
                        thread.start();
                    } 
                    else
                    {
                        String meetingID = (String) ack.receivedObj;
                        String user = NetworkServerClientRequestHandler.this.emailAppController.getUser();    

                        String meetingIDPathInServer = ResourceProvider.getRootFolderPath(ResourceProvider.SERVER_MEETING_FOLDER, user) + ResourceProvider.getFilePathSeperator() + meetingID +".json";
                        MeetingInfo meetingInfo = NetworkServerClientRequestHandler.this.emailAppController.requestMeetingInfo(meetingIDPathInServer);
                        
                        
                        for(String rec : meetingInfo.receivers)
                        {
                            //if(!rec.equals(from))
                            {
                                NetworkServerClientRequestHandler.this.sendDataToClient(rec ,ack, ResourceProvider.CLIENT_PORT);
                            }
                        }
                        
                        NetworkServerClientRequestHandler.this.sendDataToClient( meetingInfo.sender ,ack, ResourceProvider.CLIENT_PORT); 
                    
                    }
                }
        }
    }
    
    /**
     * set application request handler
     * @param emailAppController reference of email application controller or request handler
     */
    public void setAppRequestHandler(EmailApplicationRequestManager emailAppController)
    {
        this.emailAppController = emailAppController;  
    }

    /**
     * update meeting information
     * @param changes map which contains all the changes
     */
    public void updateMeetingInformation(final Map<Integer, String> changes)
    {
        final String meetingID = changes.get(MeetingInfo.MEETING_ID);

        if(!meetingUpdateChanges.containsKey(meetingID.hashCode()))
        {
            List<Map<Integer,String>> meetingUpdateChangesList = new LinkedList<Map<Integer,String>>();
            meetingUpdateChangesList.add(changes);
            meetingUpdateChanges.put(meetingID.hashCode(), meetingUpdateChangesList);
            //first time
            retryTimer = new Timer(5000, new ActionListener()
            {
                @Override
                public void actionPerformed(ActionEvent e)
                {       
                    NetworkServerClientRequestHandler.this.mergeAndUpdate(meetingID);
                }
            });
            retryTimer.setRepeats(false);
            retryTimer.start();
        }
        else
        {
            List<Map<Integer,String>> meetingUpdateChangesList = meetingUpdateChanges.get(meetingID.hashCode());
            meetingUpdateChangesList.add(changes);
        }
    }

    /**
     * to merge and update the meeting updates for the given meeting id
     * @param meetingID the meeting id
     */
    public void mergeAndUpdate(String meetingID)
    {
        // Do the merge
        // meetingUpdateChanges;
        
        String user = this.emailAppController.getUser();    
        String meetingPath = ResourceProvider.getRootFolderPath(ResourceProvider.SERVER_MEETING_FOLDER, user);
        List<MeetingInfo> meetingInfo = NetworkServerClientRequestHandler.this.emailAppController.requestAllMeetingInfo(meetingPath);
        MeetingInfo mtg = null;
        for(MeetingInfo meetings : meetingInfo)
        {
            if(meetings.meetingID.equals(meetingID))
            {
                mtg = meetings;
                break;
            }
        }
                
          if(mtg == null)
          {
              return;
          }
         
          final MeetingInfo meeting = mtg;
          String from = "";
          List<String> subjectChanges = new ArrayList<String>();
          List<String> messageChanges = new ArrayList<String>();
          List<String> startTimeChanges = new ArrayList<String>();
          List<String> endTimeChanges = new ArrayList<String>();
          List<String> locationChanges = new ArrayList<String>();
          List<Map<Integer,String>> changesList = meetingUpdateChanges.get(meetingID.hashCode());
                
          		for(Map<Integer, String> change : changesList)
                {
                    from = change.get(MeetingInfo.USER);
                    if(change.containsKey(MeetingInfo.MEETING_SUBJECT_CHANGED))
                    {
                        String newSubject = change.get(MeetingInfo.MEETING_SUBJECT_CHANGED);
                        subjectChanges.add(newSubject);
                    }
                    
                    if(change.containsKey(MeetingInfo.MEETING_MESSAGE_CHANGED))
                    {
                        String newMessage = change.get(MeetingInfo.MEETING_MESSAGE_CHANGED);
                        messageChanges.add(newMessage); 
                    }
                    
                    if(change.containsKey(MeetingInfo.MEETING_START_TIME_CHANGED))
                    {
                        String newStartTime = change.get(MeetingInfo.MEETING_START_TIME_CHANGED);
                        startTimeChanges.add(newStartTime);
                        //deprecated 
                        meeting.setOriginalStartTime(newStartTime);
                    }
                    
                    if(change.containsKey(MeetingInfo.MEETING_END_TIME_CHANGED))
                    {
                        String newEndTime = change.get(MeetingInfo.MEETING_END_TIME_CHANGED);
                        endTimeChanges.add(newEndTime);
                        //deprecated 
                        meeting.setOriginalEndiTime(newEndTime);
                    }
                    
                    if(change.containsKey(MeetingInfo.MEETING_LOCATION_CHANGED))
                    {
                        String newLocation = change.get(MeetingInfo.MEETING_LOCATION_CHANGED);
                        locationChanges.add(newLocation);
                    }
                }
          		
          		
          		
          		
          		//Updating field specific changes including conflict cases
          		
          		if(subjectChanges.size() > 0)
	          		if(subjectChanges.size()>1)
	          		{
	          			String finalSub = subjectChanges.get(0);
	          			
	          			for(int i=0 ; i<subjectChanges.size() ; i++)
	          				finalSub = Compare.merge(finalSub, subjectChanges.get(i));
	      				
	          			meeting.setOriginalSubject(finalSub);
	          		}
	          		else
	          			meeting.setOriginalSubject(subjectChanges.get(0));
          		
          		
          		if(messageChanges.size() > 0)
	          		if(messageChanges.size()>1)
	          		{
	          			String finalMessage = messageChanges.get(0);
	          			
	          			for(int i=0 ; i<messageChanges.size() ; i++)
	          			{
	          				finalMessage = Compare.merge(finalMessage, messageChanges.get(i));
	          			}
	          			meeting.setOriginalMessage(finalMessage);
	          		}
	          		else
	          			meeting.setOriginalMessage(messageChanges.get(0));
          		
          		
          		if(locationChanges.size() > 0)
	          		if(locationChanges.size()>1)
	          		{
	          			String finalLocation = locationChanges.get(0);
	          			
	          			for(int i=0 ; i<locationChanges.size() ; i++)
	          				finalLocation = Compare.merge(finalLocation, locationChanges.get(i));
	      				
	          			meeting.setOrigianlPlace(finalLocation);
	          		}
	          		else
	          			meeting.setOrigianlPlace(locationChanges.get(0));
          		
          		
          		
          		
          		meetingUpdateChanges.remove(meetingID.hashCode());
          		
          		
          		
                
                NetworkServerClientRequestHandler.this.emailAppController.requestPersistMeetingInfo(meeting, meetingPath);
                
                List<String> receivers = meeting.receivers;
                String sender = meeting.sender;
                if(!receivers.contains(sender))
                {
                    receivers.add(sender);
                }
                
                final String sendFrom = from;
                final List<String> recV = receivers;
                final Acknowledgement ack = new Acknowledgement(Acknowledgement.ACK_MEETING_UPDATED_TO_PERSIST, meeting);
                for(final String receiver : receivers)
                {
                    Thread th = new Thread()
                    {
                        public void run() 
                        {
                            
                            NetworkServerClientRequestHandler.this.sendDataToClient(receiver, ack, ResourceProvider.CLIENT_PORT);
                            
                            DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
                            Date date = new Date();
                            String dateF = dateFormat.format(date);
                            
                            Email ackEmail = new Email.Builder()
                            .setSender(sendFrom)
                            .setReceiver(recV)
                            .setSubject("Updated :" + meeting.originalSubject)
                            .setMessage("Please check the meeting board!")
                            .setTimeOfCreation(dateF)
                            .build();
                            
                            NetworkServerClientRequestHandler.this.sendDataToClient(sendFrom, ackEmail, ResourceProvider.CLIENT_PORT);
                        }
                    };
                    th.start(); 
                } 
                NetworkServerClientRequestHandler.this.sendDataToClient(meeting.sender, ack, ResourceProvider.CLIENT_PORT);
    } 

}
